| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375 |
2x
2x
2x
2x
2x
2x
2x
2x
2x
2x
2x
341x
341x
341x
341x
341x
341x
341x
341x
341x
341x
341x
2x
334x
2x
1018x
2x
353x
353x
353x
215x
215x
215x
215x
215x
208x
208x
208x
208x
13x
13x
13x
13x
13x
13x
2x
228x
2x
423x
2x
423x
423x
215x
215x
423x
2x
4x
762x
762x
762x
762x
762x
558x
558x
552x
552x
762x
2x
290x
290x
290x
287x
287x
290x
270x
270x
270x
270x
36x
36x
36x
7x
270x
2x
26x
26x
26x
26x
26x
26x
26x
17x
19x
19x
19x
19x
9x
2x
223x
223x
223x
223x
29x
29x
29x
223x
2x
100x
2x
93x
93x
93x
93x
93x
17x
17x
93x
2x
1198x
922x
928x
3048x
3048x
| /**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Query } from '../core/query';
import { SnapshotVersion } from '../core/snapshot_version';
import { Timestamp } from '../core/timestamp';
import { TargetId } from '../core/types';
import { DocumentKeySet, documentKeySet } from '../model/collections';
import { DocumentKey } from '../model/document_key';
import { assert, fail } from '../util/assert';
import { immediateSuccessor } from '../util/misc';
import * as EncodedResourcePath from './encoded_resource_path';
import { GarbageCollector } from './garbage_collector';
import {
DbTarget,
DbTargetDocument,
DbTargetDocumentKey,
DbTargetGlobal,
DbTargetGlobalKey,
DbTargetKey
} from './indexeddb_schema';
import { LocalSerializer } from './local_serializer';
import { PersistenceTransaction } from './persistence';
import { PersistencePromise } from './persistence_promise';
import { QueryCache } from './query_cache';
import { QueryData } from './query_data';
import { SimpleDbStore, SimpleDbTransaction } from './simple_db';
export class IndexedDbQueryCache implements QueryCache {
constructor(private serializer: LocalSerializer) {}
/**
* The last received snapshot version. We store this seperately from the
* metadata to avoid the extra conversion to/from DbTimestamp.
*/
private lastRemoteSnapshotVersion = SnapshotVersion.MIN;
/**
* A cached copy of the metadata for the query cache.
*/
private metadata = null;
/** The garbage collector to notify about potential garbage keys. */
private garbageCollector: GarbageCollector | null = null;
start(transaction: PersistenceTransaction): PersistencePromise<void> {
return globalTargetStore(transaction)
.get(DbTargetGlobal.key)
.next(metadata => {
assert(
metadata !== null,
'Missing metadata row that should be added by schema migration.'
);
this.metadata = metadata;
const lastSavedVersion = metadata.lastRemoteSnapshotVersion;
this.lastRemoteSnapshotVersion = SnapshotVersion.fromTimestamp(
new Timestamp(lastSavedVersion.seconds, lastSavedVersion.nanos)
);
return PersistencePromise.resolve();
});
}
getHighestTargetId(): TargetId {
return this.metadata.highestTargetId;
}
getLastRemoteSnapshotVersion(): SnapshotVersion {
return this.lastRemoteSnapshotVersion;
}
setLastRemoteSnapshotVersion(
transaction: PersistenceTransaction,
snapshotVersion: SnapshotVersion
): PersistencePromise<void> {
this.lastRemoteSnapshotVersion = snapshotVersion;
this.metadata.lastRemoteSnapshotVersion = snapshotVersion.toTimestamp();
return globalTargetStore(transaction).put(
DbTargetGlobal.key,
this.metadata
);
}
addQueryData(
transaction: PersistenceTransaction,
queryData: QueryData
): PersistencePromise<void> {
return this.saveQueryData(transaction, queryData).next(() => {
this.metadata.targetCount += 1;
this.updateMetadataFromQueryData(queryData);
return this.saveMetadata(transaction);
});
}
updateQueryData(
transaction: PersistenceTransaction,
queryData: QueryData
): PersistencePromise<void> {
return this.saveQueryData(transaction, queryData).next(() => {
Iif (this.updateMetadataFromQueryData(queryData)) {
return this.saveMetadata(transaction);
} else {
return PersistencePromise.resolve();
}
});
}
removeQueryData(
transaction: PersistenceTransaction,
queryData: QueryData
): PersistencePromise<void> {
assert(this.metadata.targetCount > 0, 'Removing from an empty query cache');
return this.removeMatchingKeysForTargetId(transaction, queryData.targetId)
.next(() => targetsStore(transaction).delete(queryData.targetId))
.next(() => {
this.metadata.targetCount -= 1;
return this.saveMetadata(transaction);
});
}
private saveMetadata(
transaction: PersistenceTransaction
): PersistencePromise<void> {
return globalTargetStore(transaction).put(
DbTargetGlobal.key,
this.metadata
);
}
private saveQueryData(
transaction: PersistenceTransaction,
queryData: QueryData
): PersistencePromise<void> {
return targetsStore(transaction).put(this.serializer.toDbTarget(queryData));
}
/**
* Updates the in-memory version of the metadata to account for values in the
* given QueryData. Saving is done separately. Returns true if there were any
* changes to the metadata.
*/
private updateMetadataFromQueryData(queryData: QueryData): boolean {
let needsUpdate = false;
if (queryData.targetId > this.metadata.highestTargetId) {
this.metadata.highestTargetId = queryData.targetId;
needsUpdate = true;
}
// TODO(GC): add sequence number check
return needsUpdate;
}
get count(): number {
return this.metadata.targetCount;
}
getQueryData(
transaction: PersistenceTransaction,
query: Query
): PersistencePromise<QueryData | null> {
// Iterating by the canonicalId may yield more than one result because
// canonicalId values are not required to be unique per target. This query
// depends on the queryTargets index to be efficent.
const canonicalId = query.canonicalId();
const range = IDBKeyRange.bound(
[canonicalId, Number.NEGATIVE_INFINITY],
[canonicalId, Number.POSITIVE_INFINITY]
);
let result: QueryData | null = null;
return targetsStore(transaction)
.iterate(
{ range, index: DbTarget.queryTargetsIndexName },
(key, value, control) => {
const found = this.serializer.fromDbTarget(value);
// After finding a potential match, check that the query is
// actually equal to the requested query.
if (query.isEqual(found.query)) {
result = found;
control.done();
}
}
)
.next(() => result);
}
addMatchingKeys(
txn: PersistenceTransaction,
keys: DocumentKeySet,
targetId: TargetId
): PersistencePromise<void> {
// PORTING NOTE: The reverse index (documentsTargets) is maintained by
// Indexeddb.
const promises: Array<PersistencePromise<void>> = [];
const store = documentTargetStore(txn);
keys.forEach(key => {
const path = EncodedResourcePath.encode(key.path);
promises.push(store.put(new DbTargetDocument(targetId, path)));
});
return PersistencePromise.waitFor(promises);
}
removeMatchingKeys(
txn: PersistenceTransaction,
keys: DocumentKeySet,
targetId: TargetId
): PersistencePromise<void> {
// PORTING NOTE: The reverse index (documentsTargets) is maintained by
// IndexedDb.
const promises: Array<PersistencePromise<void>> = [];
const store = documentTargetStore(txn);
keys.forEach(key => {
const path = EncodedResourcePath.encode(key.path);
promises.push(store.delete([targetId, path]));
if (this.garbageCollector !== null) {
this.garbageCollector.addPotentialGarbageKey(key);
}
});
return PersistencePromise.waitFor(promises);
}
removeMatchingKeysForTargetId(
txn: PersistenceTransaction,
targetId: TargetId
): PersistencePromise<void> {
const store = documentTargetStore(txn);
const range = IDBKeyRange.bound(
[targetId],
[targetId + 1],
/*lowerOpen=*/ false,
/*upperOpen=*/ true
);
return this.notifyGCForRemovedKeys(txn, range).next(() =>
store.delete(range)
);
}
private notifyGCForRemovedKeys(
txn: PersistenceTransaction,
range: IDBKeyRange
): PersistencePromise<void> {
const store = documentTargetStore(txn);
if (this.garbageCollector !== null && this.garbageCollector.isEager) {
// In order to generate garbage events properly, we need to read these
// keys before deleting.
return store.iterate({ range, keysOnly: true }, (key, _, control) => {
const path = EncodedResourcePath.decode(key[1]);
const docKey = new DocumentKey(path);
// Paranoid assertion in case the the collector is set to null
// during the iteration.
assert(
this.garbageCollector !== null,
'GarbageCollector for query cache set to null during key removal.'
);
this.garbageCollector!.addPotentialGarbageKey(docKey);
});
} else {
return PersistencePromise.resolve();
}
}
getMatchingKeysForTargetId(
txn: PersistenceTransaction,
targetId: TargetId
): PersistencePromise<DocumentKeySet> {
const range = IDBKeyRange.bound(
[targetId],
[targetId + 1],
/*lowerOpen=*/ false,
/*upperOpen=*/ true
);
const store = documentTargetStore(txn);
let result = documentKeySet();
return store
.iterate({ range, keysOnly: true }, (key, _, control) => {
const path = EncodedResourcePath.decode(key[1]);
const docKey = new DocumentKey(path);
result = result.add(docKey);
})
.next(() => result);
}
setGarbageCollector(gc: GarbageCollector | null): void {
this.garbageCollector = gc;
}
containsKey(
txn: PersistenceTransaction | null,
key: DocumentKey
): PersistencePromise<boolean> {
assert(
txn !== null,
'Persistence Transaction cannot be null for query cache containsKey'
);
const path = EncodedResourcePath.encode(key.path);
const range = IDBKeyRange.bound(
[path],
[immediateSuccessor(path)],
/*lowerOpen=*/ false,
/*upperOpen=*/ true
);
let count = 0;
return documentTargetStore(txn!)
.iterate(
{
index: DbTargetDocument.documentTargetsIndex,
keysOnly: true,
range
},
(key, _, control) => {
count++;
control.done();
}
)
.next(() => count > 0);
}
}
/**
* Helper to get a typed SimpleDbStore for the queries object store.
*/
function targetsStore(
txn: PersistenceTransaction
): SimpleDbStore<DbTargetKey, DbTarget> {
return getStore<DbTargetKey, DbTarget>(txn, DbTarget.store);
}
/**
* Helper to get a typed SimpleDbStore for the target globals object store.
*/
function globalTargetStore(
txn: PersistenceTransaction
): SimpleDbStore<DbTargetGlobalKey, DbTargetGlobal> {
return getStore<DbTargetGlobalKey, DbTargetGlobal>(txn, DbTargetGlobal.store);
}
/**
* Helper to get a typed SimpleDbStore for the document target object store.
*/
function documentTargetStore(
txn: PersistenceTransaction
): SimpleDbStore<DbTargetDocumentKey, DbTargetDocument> {
return getStore<DbTargetDocumentKey, DbTargetDocument>(
txn,
DbTargetDocument.store
);
}
/**
* Helper to get a typed SimpleDbStore from a transaction.
*/
function getStore<KeyType extends IDBValidKey, ValueType>(
txn: PersistenceTransaction,
store: string
): SimpleDbStore<KeyType, ValueType> {
Eif (txn instanceof SimpleDbTransaction) {
return txn.store<KeyType, ValueType>(store);
} else {
return fail('Invalid transaction object provided!');
}
}
|